/* dom_util.js

  Copyright (C) 2006 by Lukas Loehrer
  Parts of this file where taken from example code from  the Mozilla MDC.
  Other parts are from the clc_utils library by Charles L Chen.

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

  Author: Lukas Loehrer <loehrerl [at] gmx [dot] net>
*/

const Ci = Components.interfaces;
const Cr = Components.results;
const nsIDOMNode = Ci.nsIDOMNode;

if (typeof(module) != "undefined") {
	const logger = module.require("package", "lib/filelogger").logger;
	const xpath_util = module.require("package", "xpath_util");
}

// {{{Locating nodes

function isAtomicNode(aNode) {
	var result = false;
	var type = aNode.nodeType;
	if (type == nsIDOMNode.ELEMENT_NODE) {
		var tagName = aNode.tagName.toLower();
		
		result = (tagName == "input") || (tagName == "textarea") || (tagName == "select");
	} else if  (type == nsIDOMNode.TEXT_NODE) {
		result = true;
	} 
	return result;
}

function getFirstAncestorByTagName(aNode, aTagName ) {
	var node = aNode;
	while (node) {
		if (node.nodeName == aTagName) {
			break;
		}
		node = node.parent;
	}
	return node;
}

function findInteractiveUp(aNode) {
	while (aNode) {
		if (isInteractiveNode(aNode)) {
			return aNode;
		}
		aNode = aNode.parentNode;
	}
	return null;
}

function isInteractiveNode(aNode) {
	// We do not consider things like "onClick" attrributes at the moment

	if (aNode.nodeType != nsIDOMNode.ELEMENT_NODE) {
		return false;
	}

	var tagName = aNode.tagName.toLower();
	return (tagName == "input" && aNode.getAtrribute("type") != "hidden") ||
		(tagName == "button") ||
		(tagName == "select") ||
		(tagName == "textarea") ||
		(tagName == "a" && aNode.hasAttribute("href")) ;
}

/* Finds the frame or iframe element in the parent document of
 * aDOMWindow that points to aDOMWindow.
 */
function getFrameElementForSubWindow(aDOMWindow) {
	var parentWindow = aDOMWindow.parent;
	if (parentWindow == aDOMWindow) {
		// aDOMWindow has no parent window
		return null;
	}
	var doc = aDOMWindow.document;

	var frameElements = parentWindow.document.getElementsByTagName("frame");
	for each (var frame in frameElements) {
		if (frame.contentDocument == doc) {
			return frame;
		}
	}
	var iFrameElements = parentWindow.document.getElementsByTagName("iframe");
	for each (var frame in iFrameElements) {
		if (frame.contentDocument == doc) {
			return frame;
		}
	}
	return null;
}

// }}}
// {{{Dispatch on node type 

/* Visit all the child nodes of DOM node */

function visitDOMChildNodesRestricted(node, visitor, allowedElements) {
	var childNodes = node.childNodes;
	var result = [];
	if (childNodes) {
		for (var i = 0; i < childNodes.length; ++i) {
			var childNode = childNodes[i];
			var type = childNode.nodeType;
			if ((type == nsIDOMNode.ELEMENT_NODE || type == nsIDOMNode.TEXT_NODE) &&
				allowedElements.indexOf(childNode.nodeName) >= 0) {
				var nodeResult = visitDOMNode(childNode, visitor);
				if(nodeResult) {
					if (nodeResult instanceof Array) {
						result.push.apply(result, nodeResult);
					} else {
						result.push(nodeResult);
					}
				}
			}
		}
	}
	return result;
}

function visitDOMChildNodes(node, visitor) {
	var childNodes = node.childNodes;
	var result = [];
	if (childNodes) {
		for (var i = 0; i < childNodes.length; ++i) {
			var nodeResult = visitDOMNode(childNodes[i], visitor);
			if (nodeResult) {
				if (nodeResult instanceof Array) {
					result.push.apply(result, nodeResult);
				} else {
					result.push(nodeResult);
				}
			}
		}
	}
	return result;
}

var domNodeDispatchTable = [];
/* Initialize table */
domNodeDispatchTable[nsIDOMNode.ELEMENT_NODE] = "domVisitElement";
domNodeDispatchTable[nsIDOMNode.ATTRIBUTE_NODE] = "domVisitAttribute";
domNodeDispatchTable[nsIDOMNode.TEXT_NODE] = "domVisitText";
domNodeDispatchTable[nsIDOMNode.CDATA_SECTION_NODE] = "domVisitDataSection";
domNodeDispatchTable[nsIDOMNode.ENTITY_REFERENCE_NODE] = "domVisitEntityReference";
domNodeDispatchTable[nsIDOMNode.ENTITY_NODE] = "domVisitEntity";
domNodeDispatchTable[nsIDOMNode.PROCESSING_INSTRUCTION_NODE] = "domVisitProcessingInstruction";
domNodeDispatchTable[nsIDOMNode.COMMENT_NODE] = "domVisitComment";
domNodeDispatchTable[nsIDOMNode.DOCUMENT_NODE ] = "domVisitDocument";
domNodeDispatchTable[nsIDOMNode.DOCUMENT_TYPE_NODE] = "domVisitDocumentType";
domNodeDispatchTable[nsIDOMNode.DOCUMENT_FRAGMENT_NODE] = "domVisitDocumentFragment";
domNodeDispatchTable[nsIDOMNode.NOTATION_NODE] = "domVisitNotation";

function visitDOMNode(node, visitor) {
	var f = visitor[domNodeDispatchTable[node.nodeType]];
	if (f && f instanceof Function) {
		return f.call(visitor, node);
	}else {
		return null;
	}
}

/*
function nodeTypeDispatch(node, walker) {
	switch (node.nodeType) {
		case nsIDOMNode.ELEMENT_NODE:
			walker.domVisitElement(node);
			break;
		case nsIDOMNode.ATTRIBUTE_NODE:
			walker.domVisitAttribute(node);
			break;
		case nsIDOMNode.TEXT_NODE:
			walker.domVisitText(node);
			break;
		case nsIDOMNode.CDATA_SECTION_NODE:
			walker.domVisitDataSection(node);
			break;
		case nsIDOMNode.ENTITY_REFERENCE_NODE:
			walker.domVisitEntityReference(node);
			break;
		case nsIDOMNode.ENTITY_NODE:
			walker.domVisitEntity(node);
			break;
		case nsIDOMNode.PROCESSING_INSTRUCTION_NODE:
			walker.domVisitProcessingInstruction(node);
			break;
		case nsIDOMNode.COMMENT_NODE:
			walker.domVisitComment(node);
			break;
		case nsIDOMNode.DOCUMENT_NODE :
			walker.domVisitDocument(node);
			break;
		case nsIDOMNode.DOCUMENT_TYPE_NODE:
			walker.domVisitDocumentType(node);
			break;
		case nsIDOMNode.DOCUMENT_FRAGMENT_NODE:
			walker.domVisitDocumentFragment(node);
			break;
		case nsIDOMNode.NOTATION_NODE:
			walker.domVisitNotation(node);
			break;
		default:
	}
}
*/

// }}})
// {{{Whitespace handling 

/**
 * Throughout, whitespace is defined as one of the characters
 *  "\t" TAB \u0009
 *  "\n" LF  \u000A
 *  "\r" CR  \u000D
 *  " "  SPC \u0020
 *
 * This does not use Javascript's "\s" because that includes non-breaking
 * spaces (and also some other characters).
 */

/**
 * Determine whether a node's text content is entirely whitespace.
 *
 * @param nod  A node implementing the |CharacterData| interface (i.e.,
 *             a |Text|, |Comment|, or |CDATASection| node
 * @return     True if all of the text content of |nod| is whitespace,
 *             otherwise false.
 */
function is_all_ws( nod ) {
	// Use ECMA-262 Edition 3 String and RegExp features
	return !(/[^\t\n\r ]/.test(nod.data));
}

// From the DOM-Inspector extension
const du =Components.classes["@mozilla.org/inspector/dom-utils;1"].getService(Components.interfaces["inIDOMUtils"])
// This version is smarter, as it knows about <pre>
	// nod must be a text node
function isIgnorableWhitespace(nod) {
	return du.isIgnorableWhitespace(nod);
}

/**
 * Determine if a node should be ignored by the iterator functions.
 *
 * @param nod  An object implementing the DOM1 |Node| interface.
 * @return     true if the node is:
 *                1) A |Text| node that is all whitespace
 *                2) A |Comment| node
 *             and otherwise false.
 */

function is_ignorable( nod ) {
	return ( nod.nodeType == nsIDOMNode.COMMENT_NODE) || // A comment node
		( (nod.nodeType == nsIDOMNode.TEXT_NODE) && isIgnorableWhitespace(nod) ); // a text node, all ws
}


/**
 * Version of |previousSibling| that skips nodes that are entirely
 * whitespace or comments.  (Normally |previousSibling| is a property
 * of all DOM nodes that gives the sibling node, the node that is
 * a child of the same parent, that occurs immediately before the
 * reference node.)
 *
 * @param sib  The reference node.
 * @return     Either:
 *               1) The closest previous sibling to |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function node_before( sib ) {
	while ((sib = sib.previousSibling)) {
		if (!is_ignorable(sib)) return sib;
	}
	return null;
}

/**
 * Version of |nextSibling| that skips nodes that are entirely
 * whitespace or comments.
 *
 * @param sib  The reference node.
 * @return     Either:
 *               1) The closest next sibling to |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function node_after( sib ) {
	while ((sib = sib.nextSibling)) {
		if (!is_ignorable(sib)) return sib;
	}
	return null;
}

/**
 * Version of |lastChild| that skips nodes that are entirely
 * whitespace or comments.  (Normally |lastChild| is a property
 * of all DOM nodes that gives the last of the nodes contained
 * directly in the reference node.)
 *
 * @param sib  The reference node.
 * @return     Either:
 *               1) The last child of |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function last_child( par ) {
	var res=par.lastChild;
	while (res) {
		if (!is_ignorable(res)) return res;
		res = res.previousSibling;
	}
	return null;
}

/**
 * Version of |firstChild| that skips nodes that are entirely
 * whitespace and comments.
 *
 * @param sib  The reference node.
 * @return     Either:
 *               1) The first child of |sib| that is not
 *                  ignorable according to |is_ignorable|, or
 *               2) null if no such node exists.
 */
function first_child( par ) {
	var res=par.firstChild;
	while (res) {
		if (!is_ignorable(res)) return res;
		res = res.nextSibling;
	}
	return null;
}

/**
 * Version of |data| that doesn't include whitespace at the beginning
 * and end and normalizes all whitespace to a single space.  (Normally
 * |data| is a property of text nodes that gives the text of the node.)
 *
 * @param txt  The text node whose data should be returned
 * @return     A string giving the contents of the text node with
 *             whitespace collapsed.
 */
function data_of( txt ) {
	var data = txt.data;
	// Use ECMA-262 Edition 3 String and RegExp features
	data = data.replace(/[\t\n\r ]+/g, " ");
	if (data.charAt(0) == " ")
		data = data.substring(1, data.length);
	if (data.charAt(data.length - 1) == " ")
		data = data.substring(0, data.length - 1);
	return data;
}


const compressWhitespaceRegexp = /[\s\n\t\r]+/g;
const stripWhitespaceRegexp = /^[\s\n\t\r]*(.*?)[\s\n\t\r]*$/g;
// Compresses multiple whitespace and newlines to a single space
function compressWhitespace(s) {
	return s.replace(compressWhitespaceRegexp, " ").replace(stripWhitespaceRegexp, "$1");
} 

// }}}
// {{{Event simulation 

function simulateClick(target) {
	var doc = target.ownerDocument;
  var evt = doc.createEvent("MouseEvents");
  evt.initMouseEvent("click", true, true, doc.defaultView,
					 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  
  var canceled = !target.dispatchEvent(evt);

  return canceled;
}


function simulateChange(target) {
	var doc = target.ownerDocument;
  var evt = doc.createEvent("HTMLEvents");
  evt.initEvent("change", true, false);
  
  var canceled = !target.dispatchEvent(evt);

  return canceled;
}

// }}}
// {{{Web pgrogress listener
function SimpleWebProgressListener(handler) {
	this._handler = handler;
}


const nsIWebProgressListener = Ci.nsIWebProgressListener;
const nsIWebProgress  = Ci.nsIWebProgress;
const nsIChannel  = Ci.nsIChannel;

SimpleWebProgressListener.prototype = {
	// nsIWebProgressListener interface 
	onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
		try {
			var DOMWindow = aWebProgress.DOMWindow;
			
			if (aStateFlags & nsIWebProgressListener.STATE_START &&
				aStateFlags & nsIWebProgressListener.STATE_IS_WINDOW &&
				aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
				// This is the start event for the outer most window
				this._handler._onLoadStart(aRequest, DOMWindow);
			} 

			if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
				aRequest &&
				aStateFlags & nsIWebProgressListener.STATE_IS_WINDOW &&
				aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
					// outer most request finished

					var loadType = DOMWindow.QueryInterface(Ci.nsIInterfaceRequestor)
					.getInterface(Ci.nsIWebNavigation)
					.QueryInterface(Ci.nsIDocShell).loadType;
					// Not sure if the following two are correct
					// There is probably a cleaner way
					var isReload = ((loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD) != 0);
					var isRefresh = ((loadType & (Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH <<16)) != 0);

					// The follwoing two should be ok
					var isRestore = ((aStateFlags & nsIWebProgressListener.STATE_RESTORING) != 0);
					var isInSubFrame = (DOMWindow !=DOMWindow.top);

					this._handler._onLoadComplete(aRequest, DOMWindow, isInSubFrame, isRestore, isReload, isRefresh);
				}
		} catch (e) {
			logger.error("SimpleWebProgressListener: " + e, e);
		}
		return;
	},

	onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
							   aMaxTotalProgress) {
							   },

	onLocationChange: function(aWebProgress, aRequest, aLocation) {
		if(aRequest == null) {
			// Presumably, its a jump to an anchor within a page
			this._handler._onInternalJump(aLocation, aWebProgress.DOMWindow);
		}
	},

	onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) {
	},

	onSecurityChange: function(aWebProgress, aRequest, state) {
	},

	// nsISupports interface
	QueryInterface: function(aIID) {
		if (aIID.equals(Components.interfaces.nsIWebProgressListener)   ||
			aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
			aIID.equals(Components.interfaces.nsISupports))
		return this;
		throw Components.results.NS_NOINTERFACE;
	}
};
	



// }}}
// {{{Finding nodes for named anchors

function findNamedAnchor(hash, rootNode) {
	var queryString = '//a[@name = "' + hash + '"]|//*[@id =  "' + hash + '"]';
	var result = xpath_util.queryForSnapshot(rootNode , queryString);
	if (result.snapshotLength > 0) {
		return  result.snapshotItem(0); // Just use first match for now
	}  else {
		return null;
	}
}

function possibleTargetNodeFromURI(aURI, aRootNode) {
	var targetNode = null;
	try {
		var url = aURI.QueryInterface(Ci.nsIURL);
		var hash = url.ref;
		if  (hash && hash != "") {
			targetNode = findNamedAnchor(hash, aRootNode);
		}
	} catch (e) {}
	return targetNode;
}

	// }}}
// {{{HTML tables
/* The followingn functions were taken from clc_utils by Charles L Chen.
   They were adapted to return the DOM nodes instead of node contents.
 */

//Returns true if the target table is most likely a data table;
//false if the target table is most likely a layout table.
//This function is not guaranteed to be correct at guessing
//the intent of a table; however, it uses some good heuristics
//to make a pretty good guess most of the time.
//
function probablyDataTable(target){
	//Assume it is a data table if there is a summary attribute
	if (target.summary){
		return true;
	}
	//Assume it is a data table if there are TH elements
	else if (target.getElementsByTagName("th").length){
		return true;
	}
	//Assume it is a data table if there are TD elements that have a headers attribute in them
	else {
		for (var i = 0; i < target.getElementsByTagName("td").length; i++){
			if (target.getElementsByTagName("td")[i].headers != ""){
				return true;
			}
		}
	}
	//If it has not returned by now, it is probably a layout table
	return false;
}

//Returns the string that is marked as the col heading for the cell.
//If there is nothing marked explicitly as the col heading,
//the return value will be null.
//If this returns null, use CLC_GuessColHeading to 
//return a guessed value that might be the col heading.
//
function getColHeading(target){
	var row_index = target.cellIndex;
	var tempnode = target;
	while (tempnode && tempnode.localName.toLowerCase() != "tbody"){
		tempnode = tempnode.parentNode;
	}
	if (!tempnode){
		return null;
	}
	var rows = tempnode.getElementsByTagName("tr"); 
	if (!rows[0]){
		return null;
	}
	if (!rows[0].cells[row_index]){
		return null;
	}   
	if (!(rows[0].cells[row_index].localName.toLowerCase() == "th")){
		return null;
	}
	return rows[0].cells[row_index];
}

//Returns the string that is guessed to be the col heading for the node.
//If there is no guess that makes sense, the return value will be null.
//Note: If the table is guessed to be a layout table, then there will
//be no sensible guess and null will be returned.
//
function guessColHeading(target){
	var row_index = target.cellIndex;
	var tempnode = target;
	while (tempnode && tempnode.localName.toLowerCase() != "tbody"){
		tempnode = tempnode.parentNode;
	}
	if (!tempnode){
		return null;
	}
	if (!probablyDataTable(tempnode)){
		return null;
	}
	var rows = tempnode.getElementsByTagName("tr"); 
	if (!rows[0]){
		return null;
	}
	if (!rows[0].cells[row_index]){
		return null;
	}   
	return rows[0].cells[row_index];
}


//------------------------------------------
//Returns the string that is marked as the row heading for the cell.
//If there is nothing marked explicitly as the row heading,
//the return value will be null.
//If this returns null, use CLC_GuessRowHeading to 
//return a guessed value that might be the row heading.
//
function getRowHeading(target){
	var tempnode = target;
	while (tempnode && tempnode.localName.toLowerCase() != "tr"){
		tempnode = tempnode.parentNode;
	}
	if (!tempnode){
		return null;
	}
	var rowheader = tempnode.getElementsByTagName("th"); 
	if (!rowheader[0]){
		return null;
	}
	return rowheader[0];
}

//------------------------------------------
//Returns the string that is guessed to be the row heading for the cell.
//If there is no guess that makes sense, the return value will be null.
//Note: If the table is guessed to be a layout table, then there will
//be no sensible guess and null will be returned.
//
function guessRowHeading(target){
	//Check if this is a data table first
	var tempnode = target;
	while (tempnode && tempnode.localName.toLowerCase() != "tbody"){
		tempnode = tempnode.parentNode;
	}
	if (!pobablyDataTable(tempnode)){
		return null;
	}
	//Guess the row heading by assuming that it is the first cell in the row
	tempnode = target;
	while (tempnode && tempnode.localName.toLowerCase() != "tr"){
		tempnode = tempnode.parentNode;
	}
	if (!tempnode){
		return null;
	}
	var cells = tempnode.getElementsByTagName("td"); 
	if (cells.length < 1){
		return null;
	}
	return cells[0];
}
 
// }}}


// {{{ Emacs local variables

// local variables:
// folded-file: t
// end:

// }}}
